package org.unidata.mdm.data.service.impl;

import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.serialization.CoreSerializer;
import org.unidata.mdm.core.type.formless.BundlesArray;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.context.RelationFromIdentityContext;
import org.unidata.mdm.data.context.RelationIdentityContext;
import org.unidata.mdm.data.context.RelationToIdentityContext;
import org.unidata.mdm.data.service.segments.relations.draft.RelationDraftGetStartExecutor;
import org.unidata.mdm.data.service.segments.relations.draft.RelationDraftPublishStartExecutor;
import org.unidata.mdm.data.service.segments.relations.draft.RelationDraftUpsertStartExecutor;
import org.unidata.mdm.data.type.draft.DataDraftParameters;
import org.unidata.mdm.data.type.draft.DataDraftTags;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.draft.context.DraftQueryContext;
import org.unidata.mdm.draft.context.DraftUpsertContext;
import org.unidata.mdm.draft.dto.DraftQueryResult;
import org.unidata.mdm.draft.dto.DraftUpsertResult;
import org.unidata.mdm.draft.service.DraftService;
import org.unidata.mdm.draft.type.DraftOperation;
import org.unidata.mdm.draft.type.DraftProvider;
import org.unidata.mdm.draft.type.DraftTags;
import org.unidata.mdm.meta.type.RelativeDirection;
import org.unidata.mdm.system.context.DraftAwareContext;
import org.unidata.mdm.system.context.DraftIdResettingContext;
import org.unidata.mdm.system.service.TextService;

/**
 * @author Mikhail Mikhailov on Sep 25, 2020
 */
@Component
public class RelationDraftProviderComponent implements DraftProvider<BundlesArray> {
    /**
     * This draft provider id.
     */
    public static final String ID = "relation";
    /**
     * This draft provider description.
     */
    private static final String DESCRIPTION = "app.data.draft.relation.provider.description";
    /**
     * The TS.
     */
    @Autowired
    private TextService textService;
    /**
     * The DS.
     */
    private DraftService draftService;
    /**
     * Constructor.
     */
    @Autowired
    public RelationDraftProviderComponent(DraftService draftService) {
        super();
        this.draftService = draftService;
        this.draftService.register(this);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getId() {
        return ID;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getDescription() {
        return textService.getText(DESCRIPTION);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<String> getTags() {
        return DataDraftTags.RELATION_SYSTEM_TAGS;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getPipelineId(DraftOperation operation) {

        switch (operation) {
        case GET_DATA:
            return RelationDraftGetStartExecutor.SEGMENT_ID;
        case UPSERT_DATA:
            return RelationDraftUpsertStartExecutor.SEGMENT_ID;
        case PUBLISH_DATA:
            return RelationDraftPublishStartExecutor.SEGMENT_ID;
        }

        return null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public BundlesArray fromBytes(byte[] data) {
        return CoreSerializer.bundlesArrayFromProtostuff(data);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] toBytes(BundlesArray data) {
        return CoreSerializer.bundlesArrayToProtostuff(data);
    }
    /**
     * Tries to select draft id for the given context.
     * @param <C> the context type
     * @param ctx the context instance
     * @return draft id or null, if not found
     */
    @Nullable
    public <C extends DraftAwareContext & RelationIdentityContext> Long selectDraftId(C ctx) {

        // Return DID
        if (Objects.nonNull(ctx) && ctx.hasDraftId()) {
            return ctx.getDraftId();
        }

        // Resolve draft id by parent draft id
        if (Objects.nonNull(ctx) && ctx.hasParentDraftId()) {

            // Try subject (relation etalon ID) first
            DraftQueryResult result = probeSubjectId(ctx);

            // Try side identity second
            if (Objects.isNull(result)) {
                result = probeSideIdentity(ctx);
            }

            return result != null && result.getDrafts().size() == 1
                    ? result.getDrafts().get(0).getDraftId()
                    : null;
        }

        return null;
    }

    /**
     * If draft id can be resolved using this context, it is used.
     * If not, it is CREATED and the returned.
     * @param <C> the context type
     * @param ctx the context
     * @return draft id
     */
    public<C extends DraftAwareContext & DraftIdResettingContext & RelationIdentityContext> Long ensureDraftId(C ctx) {

        // Find
        Long existing = selectDraftId(ctx);
        if (Objects.nonNull(existing)) {

            // Set to context, if found by parent id, but not set
            if (!ctx.hasDraftId()) {
                ctx.setDraftId(existing);
            }

            return existing;
        }

        // Or create (provoke NPE if relation name is not set)
        RelationKeys keys = ctx.relationKeys();

        String subjectId = Objects.isNull(keys) ? StringUtils.EMPTY : keys.getEtalonKey().getId();
        String relationName = Objects.isNull(keys) ? ctx.relationName() : keys.getRelationName();

        Objects.requireNonNull(relationName, "Relation name can not be null, while creating relation drafts.");

        DraftUpsertResult result = draftService.upsert(DraftUpsertContext.builder()
                .provider(ID)
                .subjectId(subjectId)
                .parameter(DataDraftParameters.ENTITY_NAME, relationName)
                .parentDraftId(ctx.getParentDraftId())
                .owner(SecurityUtils.getCurrentUserName())
                .build());

        ctx.setDraftId(result.getDraft().getDraftId());

        return result.getDraft().getDraftId();
    }

    private<C extends DraftAwareContext & RelationIdentityContext>  DraftQueryResult probeSideIdentity(C ctx) {

        Collection<String> tags = identityTags(ctx);
        if (tags.size() == 3) {

            return draftService.drafts(DraftQueryContext.builder()
                    .parentDraftId(ctx.getParentDraftId())
                    .provider(ID)
                    .tags(tags)
                    .build());
        }

        return null;
    }

    private<C extends DraftAwareContext & RelationIdentityContext>  DraftQueryResult probeSubjectId(C ctx) {

        String subjectId;
        RelationKeys keys = ctx.relationKeys();
        if (Objects.nonNull(keys)) {
            subjectId = keys.getEtalonKey().getId();
        } else {
            subjectId = ctx.isRelationEtalonKey() ? ctx.getRelationEtalonKey() : null;
        }

        if (Objects.nonNull(subjectId)) {

            return draftService.drafts(DraftQueryContext.builder()
                    .parentDraftId(ctx.getParentDraftId())
                    .provider(ID)
                    .subjectId(subjectId)
                    .build());
        }

        return null;
    }

    private<C extends DraftAwareContext & RelationIdentityContext> Collection<String> identityTags(C ctx) {

        Set<String> tags = new HashSet<>();
        if (ctx.getDirection() == RelativeDirection.FROM) {

            if (ctx.isEtalonRecordKey()) {
                tags.add(DraftTags.toTag(DataDraftTags.RELATION_TO_ETALON_ID, ctx.getEtalonKey()));
            } else if (ctx.isOriginExternalId()) {
                tags.add(DraftTags.toTag(DataDraftTags.RELATION_TO_EXTERNAL_ID, ctx.getExternalIdAsObject().compact()));
            }

            RecordKeys from = ((RelationFromIdentityContext) ctx).fromKeys();
            if (Objects.nonNull(from)) {
                tags.add(DraftTags.toTag(DataDraftTags.RELATION_FROM_ETALON_ID, from.getEtalonKey().getId()));
            }

        } else if (ctx.getDirection() == RelativeDirection.TO) {

            if (ctx.isEtalonRecordKey()) {
                tags.add(DraftTags.toTag(DataDraftTags.RELATION_FROM_ETALON_ID, ctx.getEtalonKey()));
            } else if (ctx.isOriginExternalId()) {
                tags.add(DraftTags.toTag(DataDraftTags.RELATION_FROM_EXTERNAL_ID, ctx.getExternalIdAsObject().compact()));
            }

            RecordKeys to = ((RelationToIdentityContext) ctx).toKeys();
            if (Objects.nonNull(to)) {
                tags.add(DraftTags.toTag(DataDraftTags.RELATION_TO_ETALON_ID, to.getEtalonKey().getId()));
            }
        }

        String relationName = ctx.relationName();
        if (Objects.nonNull(relationName)) {
            tags.add(DraftTags.toTag(DataDraftTags.ENTITY_NAME, relationName));
        }

        return tags;
    }
}
